3.6. A2A
What is in-process delegation?
An ADK coordinator can transfer control to a named sub-agent in the same process and session. The course's diagnosis specialist owns only read and runbook tools, the remediation specialist owns only the guarded actions, and the coordinator owns triage tools and decides when to delegate:
coordinator_agent = Agent(
model=build_model(),
name="coordinator_agent",
description="On-call coordinator that triages incidents and delegates diagnosis and remediation.",
tools=ALL_TOOLS,
sub_agents=[diagnosis_agent, remediation_agent],
)
The complete definitions in delegation.py attach the instructions, redaction, and stable error callbacks; 3.7. Multi-Agent covers the delegation pattern and its least-privilege boundaries in depth. In-process delegation is simple and low latency, but both agents share deployment, failure, trust, and scaling boundaries.
What does A2A add?
A2A exposes an agent across a network boundary. A client first reads an agent card, then submits/streams tasks according to the protocol. Independent deployment allows separate ownership and scaling but adds identity, network policy, compatibility, retries, persistence, and observability concerns.
In this repository, the in-process coordinator demonstrates delegation semantics. agent.server exposes the root Ops Copilot over A2A. It does not pretend that the in-process specialist is already a separately deployed remote agent.
What does the agent card declare?
agent_card = AgentCard(
name="Ops Copilot",
description="Runbook-grounded incident triage and guarded remediation for the AgentOps Open Course.",
url=f"{settings.a2a_protocol}://{settings.a2a_host}:{settings.a2a_port}/",
version=version("agentops-agent"),
capabilities=AgentCapabilities(streaming=True, state_transition_history=True),
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
skills=[
AgentSkill(
id="incident-triage",
name="Incident triage",
description="Prioritize incidents using service state, logs, and deterministic severity rules.",
tags=["incident", "triage", "operations"],
examples=["Triage the open incidents."],
),
],
)
The source constructs both AgentSkill records inline; the excerpt shows the first. The card must advertise a client-reachable service address, never the listener address 0.0.0.0.
How does the server preserve tasks?
create_app() builds an explicit runner with DatabaseSessionService and an A2A DatabaseTaskStore, both backed by .state/runtime.db through one single-connection SQLAlchemy pool. Serializing those short writes avoids competing SQLite writers. Its lifespan closes the runner and the session service that owns the shared engine. This is a single-process course durability model, not a horizontally scalable database design.
How do you inspect the A2A contract?
In another terminal:
Expected name: Ops Copilot; the card should list incident triage and guarded remediation.
How do I stream a response through the gateway?
Call message/stream instead of message/send. The response is an SSE channel: every data: line is one JSON-RPC response carrying a whole task event (submitted, working, completed, failed). With the A2A server (mise run a2a), the gateway (5.3. A2A Gateway), and a model backend running:
curl -N -fsS http://127.0.0.1:3001/ \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0", "id": "1", "method": "message/stream",
"params": {"message": {"kind": "message", "messageId": "demo-1",
"role": "user", "parts": [{"kind": "text", "text": "Triage the open incidents."}]}}
}'
You always get this event stream — it reports task progress, not model tokens. Whether the model also streams is a separate, opt-in decision made server-side in server.py:
updates: dict[str, object] = {"max_llm_calls": settings.a2a_max_llm_calls}
if settings.a2a_streaming:
updates["streaming_mode"] = StreamingMode.SSE
converted.run_config = run_config.model_copy(update=updates)
By default the model runs non-streaming and the stream carries whole events. AGENT_A2A_STREAMING=true adds partial per-token events for lower perceived latency, at the redaction cost described below — which is why it defaults to false.
What happens when a stream fails mid-answer?
The stream terminates explicitly, not silently. When the run raises mid-stream, the executor emits a terminal failed status-update event (final: true) on the same channel — a client sees the task fail rather than a hung connection. The course's web client (5.3. A2A Gateway) keys off exactly these terminal states.
Redaction has a precise contract under streaming. redact_response_pii is an after_model_callback, and ADK runs it on every partial chunk and on the final aggregated response — enabling streaming does not bypass the PII guardrail (4.5. Guardrails). But per-chunk redaction is best-effort: an entity split across two chunk boundaries may not match either fragment, and fragments already sent cannot be retracted. The final aggregate is redacted as a whole, so the durable answer is clean; only the transient partial view carries that residual risk. This asymmetry is the reason AGENT_A2A_STREAMING defaults to false and is an explicit trade-off, not a free feature.
When should an agent become a separate service?
Split it when ownership, data classification, scaling, release cadence, or blast radius differs enough to justify the network boundary. Do not split merely to draw a multi-agent diagram: a function or in-process agent is cheaper when lifecycle and trust are identical.
What is the A2A checkpoint?
Verify card fields, advertised address, persistent service/task construction, shutdown cleanup, the coordinator's sub-agent wiring, and the real input-required confirmation response resuming the same task with attributable audit evidence. A deterministic fake model drives that approval round trip, so no live model call is needed.